/**
 *  Copyright 2002-2009 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package foo.bar.wiki.services;

import foo.bar.wiki.domain.Permission;
import foo.bar.wiki.domain.Role;
import foo.bar.wiki.domain.User;
import foo.bar.wiki.domain.Page;
import foo.bar.wiki.security.encryption.Encryptor;
import foo.bar.wiki.security.UserAccountRealm;
import foo.bar.wiki.services.exception.UserAndEmailCombinationDoesNotExistException;
import foo.bar.wiki.services.exception.UserAlreadyExistsException;
import foo.bar.wiki.services.exception.NoSuchUserException;
import foo.bar.wiki.services.exception.CannotAddFavouritePageToAnonymousUserException;
import foo.bar.wiki.web.action.ExtendedPaginatedList;
import foo.bar.wiki.plugins.PluginManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.MessageSource;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.orm.jpa.JpaTemplate;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.Iterator;

import net.sourceforge.stripes.validation.ValidationErrors;
import net.sourceforge.stripes.action.Message;

/**
 * Service that handles {@link foo.bar.wiki.domain.User} related activities.
 *
 * @author tmjee
 * @version $Date$ $Id$
 */
public class UserService extends AbstractJpaAwareService {

    private Encryptor encryptor;
    private MailSender mailSender;
    private MessageSource messageSource;

    private SimpleMailMessage forgottenPasswordMailMessage;

    private static final Log LOG = LogFactory.getLog(UserService.class);

    private PluginManager pluginManager;

    UserService(){}
    public UserService(JpaTemplate template, Encryptor encryptor, MailSender mailSender, MessageSource emailMessageSource,
                       PluginManager pluginManager) {
        super(template);
        this.encryptor = encryptor;
        this.mailSender = mailSender;
        this.messageSource = emailMessageSource;
        this.pluginManager = pluginManager;
    }



    public Encryptor getEncryptor() {
        return encryptor;
    }


    public void setForgottenPasswordMailMessage(SimpleMailMessage forgottenPasswordMailMessage) {
        this.forgottenPasswordMailMessage = forgottenPasswordMailMessage;
    }



    public void signup(User user, List<Message> messages) {
        pluginManager.getUserRegistrationPlugin().register(user, messages);
    }

    public void validateSignup(User user, ValidationErrors errors, List<Message> messages) {
        pluginManager.getUserRegistrationPlugin().validate(user, errors, messages);    
    }


    
    /**
     * Update only when user exists, else throw NoSuchUserException
     * @param user
     * @return
     */
    public User updateUser(final User user) throws NoSuchUserException {
        return (User) getJpaTemplate().execute(new JpaCallback()    {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                Query q = entityManager.createNamedQuery("User_findByUsername");
                q.setParameter("username", user.getUsername());
                Object rst = getFirstResult(q);

                if (rst == null) {
                    throw new NoSuchUserException("User with ["+user.getUsername()+"] doesn't exists");
                } else {
                    User usr =  (User) saveOrUpdate(entityManager, user);
                    return usr;
                }
            }
        });
    }


    /**
     * Save only if it's a new user (doesn't exists before) else throw {@link UserAlreadyExistsException}.
     * @param user
     * @return
     */
    public User saveUser(final User user) throws UserAlreadyExistsException {
        return (User) getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                Query _tmp = entityManager.createNamedQuery("User_findByUsername");
                _tmp.setParameter("username", user.getUsername());
                Object rst = getFirstResult(_tmp);

                if (null == rst) {
                    return saveOrUpdate(entityManager, user);
                }
                else {
                    throw new UserAlreadyExistsException("User with ["+user.getUsername()+"] already exists");
                }
            }
        });
    }


    /**
     * Save or update a {@link foo.bar.wiki.domain.User}.
     * @see {@link #saveOrUpdate(javax.persistence.EntityManager, foo.bar.wiki.domain.Identifiable)}
     * @param user
     * @return
     */
    public User saveOrUpdateUser(final User user) {
        return (User) getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                return saveOrUpdate(entityManager, user);
            }
        });
    }

    /**
     * Returns a {@link foo.bar.wiki.domain.User} with the <code>username</code> and <code>email</code> given.
     * @param username
     * @param email
     * @return
     */
    public User getUserByUsernameAndEmailAddress(final String username, final String email) {
        return (User) getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                Query query = entityManager.createNamedQuery("User_findByUsernameAndEmail");
                query.setParameter("username", username);
                query.setParameter("email", email);
                return (User) getFirstResult(query);
            }
        });
    }


    /**
     * Returns a {@link foo.bar.wiki.domain.User} with the give <code>userId</code>.
     * @param userId
     * @return
     */
    public User getUserById(final String userId) {
        return (User) getJpaTemplate().execute(new JpaCallback(){
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                Query query = entityManager.createNamedQuery("User_findByUserId");
                query.setParameter("userId", userId);
                return getFirstResult(query);
            }
        });
    }

    /**
     * Return true if the <code>username</code> exists.
     * @param username
     * @return
     */
    public boolean isUsernameExists(final String username) {
        return (Boolean) getJpaTemplate().execute(new JpaCallback(){
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                Query query = entityManager.createNamedQuery("User_findByUsername");
                query.setParameter("username", username);
                return (query.getResultList().size() > 0);
            }
        });
    }

    /**
     * Returns an {@link foo.bar.wiki.web.action.ExtendedPaginatedList} of all {@link foo.bar.wiki.domain.User}s.
     * @param paginatedList
     * @return
     */
    public ExtendedPaginatedList getAllUsers(final ExtendedPaginatedList paginatedList) {
        return (ExtendedPaginatedList) getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                Query query1 = entityManager.createNamedQuery("User_countAllUsers");
                long total = (Long) getFirstResult(query1);

                Query query2 = entityManager.createNamedQuery("User_findAllUsers");
                prepareQueryWithPaginatedListInfo(paginatedList, query2);
                
                return populatePagniatedList(paginatedList, query2.getResultList(), total);
            }
        });
    }

    /**
     * Delete the {@link foo.bar.wiki.domain.User}.
     * @param user
     */
    public void deleteUser(final User user) {
        getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                User u = entityManager.merge(user);
                entityManager.remove(u);
                return null;
            }
        });
    }


    /**
     * Return the {@link foo.bar.wiki.domain.Role} having the given <code>roleId</code>
     * @param roleId
     * @return
     */
    public Role getRoleById(final String roleId) {
        return (Role) getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                return (Role) entityManager.find(Role.class, roleId);
            }
        });
    }

    /**
     * Return the {@link foo.bar.wiki.domain.Permission} having the given <code>permissionId</code>
     * @param permissionId
     * @return
     */
    public Permission getPermissionById(final String permissionId) {
        return (Permission) getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                return (Permission) entityManager.find(Permission.class, permissionId);
            }
        });
    }

    /**
     * Delete all the <code>roles</code>
     * @param roles
     */
    public void deleteRole(final List<Role> roles) {
        getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                for (Role role: roles) {
                    Role r = entityManager.merge(role);
                    entityManager.remove(r);
                }
                return null;
            }
        });
    }

    /**
     * Delete all the <code>permissions</code>.
     * @param permissions
     */
    public void deletePermission(final List<Permission> permissions) {
        getJpaTemplate().execute(new JpaCallback() {
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                for (Permission permission :permissions) {
                    Permission p = entityManager.merge(permission);
                    entityManager.remove(p);
                }
                return null;
            }
        });
    }


    /**
     * Perform forgotten password action :-
     * <ul>
     *  <li>Generates a random password</li>
     *  <li>Send out an email to the user with the new password</li>
     *  <li>Reset the user's password to the new random password</li>
     * </ul>
     * Will throw {@link foo.bar.wiki.services.exception.UserAndEmailCombinationDoesNotExistException} if the
     * username and email combination doesn't exists.
     *
     * @param username
     * @param email
     * @param locale
     * @return
     */
    public String performForgottenPasswordAction(final String username, final String email, final Locale locale) {
        return (String) getJpaTemplate().execute(new JpaCallback(){
            public Object doInJpa(EntityManager entityManager) throws PersistenceException {
                    User user = getUserByUsernameAndEmailAddress(username, email);
                    if (user == null) {
                        throw new UserAndEmailCombinationDoesNotExistException("username ["+username+"] email ["+email+"] combination doesn't exists");
                    }
                    String newPassword = UUID.randomUUID().toString();
                    forgottenPasswordMailMessage.setSubject(messageSource.getMessage("forgottenPasswordEmail.subject", new Object[] { username }, locale));
                    forgottenPasswordMailMessage.setTo(new String[] {
                            user.getEmail()
                    });
                    String message = messageSource.getMessage(
                            "forgottenPasswordEmail.content",
                            new Object[] {
                                    user.getUsername(), // username
                                    user.getEmail(), // email address
                                    newPassword // new password
                            },
                            locale);
                    forgottenPasswordMailMessage.setText(message);
                    mailSender.send(forgottenPasswordMailMessage);

                    // if we reach here everything is ok, we can set reset the password
                    user.setPassword(newPassword);
                return newPassword;
            }
        });
    }


    protected String generateRandomPassword() {
        return UUID.randomUUID().toString();
    }

    public void addPageToUserFavourite(User user, Page page) throws CannotAddFavouritePageToAnonymousUserException {
        if (user != UserAccountRealm.ANONYMOUS_USER_ACCOUNT.getUser()) {
            User _user = getUserById(user.getId());
            if (!_user.getFavouritePages().contains(page)) {
                _user.getFavouritePages().add(page);
            }
        }
        else {
            throw new CannotAddFavouritePageToAnonymousUserException("Cannot add page ["+page.getName()+"] to Anonymous user account");
        }
    }

    public User changePassword(User user, String password) throws NoSuchUserException {
        String encrypted_password = encryptor.encrypt(password);
        user.setPassword(encrypted_password);
        return updateUser(user);
    }
}
